using System;
using System.Collections;
using System.Collections.Specialized;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using System.Threading;
using Mediasock.Client.Attribute;
using Mediasock.Client.Service;
using Mediasock.Library.File;

namespace Vimeo
{
    public class VimeoService : Service
    {
        #region private_fields

        // Service API stuff
        private string _apiKey;
        private string _apiSecret;

        // Authentication stuff
        private string _authToken;

        // URLs
        string _urlRest;
        string _urlUpload;
        string _urlAuth;

        // Requests and responses
        private XmlNodeList _responseNodes;

        // Taxa and objects
        private AttributesCollection _taxa;
        private AttributesCollection _objects;



        #endregion

        public VimeoService(string serviceId)
            : this(serviceId, new AttributesRecord())
        {
        }

        public VimeoService(string serviceId, AttributesRecord configAttributes)
            : base(serviceId, configAttributes)
        {
            // Retrieve the values from config
            _apiKey = this.configAttributes.Contains("com.vimeo:api_key") ? this.configAttributes["com.vimeo:api_key"] : "";
            _apiSecret = this.configAttributes.Contains("com.vimeo:secret") ? this.configAttributes["com.vimeo:secret"] : "";

            string server = this.configAttributes.Contains("com.vimeo:server") ? this.configAttributes["com.vimeo:server"] : "http://www.vimeo.com/";
            this.updateUrls(server);
        }

        public override void SetSession(AttributesCollection sessionAttributesCollection)
        {
            // Get the album collection
            _taxa = sessionAttributesCollection.Find(AttributesType.Taxa);

            // Get the globlal attributes
            AttributesCollection collection = sessionAttributesCollection.Find(AttributesType.Sock);

            if (collection.Count > 1)
            {
                AttributesRecord record = collection[0]; // Take the first one
                _apiKey = record["com.vimeo:api_key"];
                _apiSecret = record["com.vimeo:secret"];
                _authToken = record["auth:pass"];
            }
        }

        public override AttributesCollection GetSession()
        {
            AttributesCollection collection = new AttributesCollection();

            // Add the global variables
            AttributesRecord record = new AttributesRecord(AttributesType.Sock);
            record["com.vimeo:api_key"] = _apiKey;
            record["com.vimeo:secret"] = _apiSecret;
            record["auth:pass"] = _authToken;
            collection.Add(record);

            // Add the album collection
            collection.AddRange(_taxa);

            return collection;
        }

        public override AttributesRecord SignIn(AttributesRecord attributes)
        {
            // Not signed in
            if (_authToken == null)
            {
                if (attributes.Contains("auth:pass"))
                    _authToken = attributes["auth:pass"];

                else
                {
                    if (attributes.Contains("auth:frob"))
                        return signIn(attributes["auth:frob"]);
                    else
                        return signIn("");
                }
            }

            // Tokens have permanenty validity, no need to re-signin
            return this.makeStatusRecord("0", null);
        }

        public override AttributesRecord SignOut()
        {
            return this.makeStatusRecord("11", "Method not implemented");
        }

        public override AttributesCollection FindTaxa(AttributesRecord searchAttributes)
        {
            return new AttributesCollection(this.makeStatusRecord("11", "Method not implemented"));
        }

        public override AttributesCollection MakeTaxa(AttributesRecord taxaAttributes)
        {
            return new AttributesCollection(this.makeStatusRecord("11", "Method not implemented"));
        }

        public override AttributesCollection FindObjects(int index, int count, AttributesRecord searchAttributes, int precache)
        {
            // Using a one-based index
            if (index < 1)
                return new AttributesCollection(this.makeStatusRecord("10", "Index out of range."));

            AttributesRecord status = this.makeStatusRecord("0", null);

            int finalPhoto = index + count - 1; //Index is inclusive of first object

            if (_objects == null)
                _objects = new AttributesCollection();

            bool success = true;

            try
            {
                this.findObjectsPaginated(1, index + count);
            }
            catch (Exception e)
            {
                Console.WriteLine("FindObjects exception: " + e.Message);
                success = false;
            }

            AttributesCollection collection = new AttributesCollection();

            if (!success)
                collection.Add(this.makeStatusRecord("50", "Error finding objects"));
            else
            {
                int firstObjectIndex = index - 1;
                int finalObjectIndex = ((index + count - 1) < _objects.Count - 1) ? index + count - 1 : _objects.Count - 1;

                for (int i = firstObjectIndex; i <= finalObjectIndex; i++)
                    collection.Add(_objects[i]);

                collection.Add(this.makeStatusRecord("0", null));
            }

            return collection;
        }

        public override AttributesCollection ReceiveObjects(AttributesCollection objectAttributesCollection, string downloadPath)
        {
            return new AttributesCollection(this.makeStatusRecord("11", "Method not implemented"));
        }

        public override AttributesCollection SendObjects(AttributesCollection objectsAttributesCollection)
        {
            int failureCount = 0;

            foreach (AttributesRecord objectRecord in objectsAttributesCollection)
            {
                objectRecord.Type = AttributesType.Object;

                FileStream fileStream = new FileStream(objectRecord["file:original"].ToString(), FileMode.Open);

                this.uploadObject(objectRecord, (Stream)fileStream);

                if (objectRecord["status:code"] != "0")
                    failureCount++;
            }

            // Adding an status denoting the overall success
            AttributesRecord status = new AttributesRecord(AttributesType.Sock);

            if (failureCount > 0)
                status = (failureCount == objectsAttributesCollection.Count) ? this.makeStatusRecord("50", null) : this.makeStatusRecord("03", null);
            else
                status = this.makeStatusRecord("0", null);

            objectsAttributesCollection.Add(status);

            return objectsAttributesCollection;
        }

        public override AttributesRecord SendObject(AttributesRecord objectAttributes, Stream fileStream)
        {
            objectAttributes.Type = AttributesType.Object;

            this.uploadObject(objectAttributes, fileStream);

            return objectAttributes;
        }

        public override AttributesCollection GetMetadata(AttributesCollection objects)
        {
            int failureCount = 0;

            foreach (AttributesRecord objectRecord in objects.Find(AttributesType.Object))
            {
                try
                {
                    this.findObjectInfo(objectRecord);

                    this.findObjectThumbnailUrl(objectRecord);
                }
                catch (Exception e)
                {
                    failureCount++;
                }
            }

            // Adding an status denoting the overall success
            AttributesRecord status = new AttributesRecord(AttributesType.Sock);

            if (failureCount > 0)
                status = (failureCount == objects.Count) ? this.makeStatusRecord("50", null) : this.makeStatusRecord("03", null);
            else
                status = this.makeStatusRecord("0", null);

            objects.Add(status);

            return objects;
        }


        #region authentication

        private AttributesRecord signIn(string frob)
        {
            AttributesRecord status = new AttributesRecord();

            // If no frob, then fetch the frob first
            if (frob == "")
            {
                AttributesRecord frobRecord = this.getFrob();

                status = frobRecord;

                if (frobRecord["status:code"] == "0")
                {
                    status = this.makeStatusRecord("0", null);
                    status.Merge(frobRecord);
                    status["auth:url"] = this.getAuthorizationUrl(frobRecord["auth:frob"]);
                }
                else // Error
                    status = frobRecord;
            }

            // If frob exists, get a token
            else
            {
                AttributesRecord token = this.getToken(frob);
                if (token["status:code"] == "0")
                {
                    status = this.makeStatusRecord("0", null);

                    _authToken = token["auth:token"];
                }
                else // Error
                    status = token;
            }

            return status;
        }

        private AttributesRecord getFrob()
        {
            Hashtable parameters = new Hashtable();

            parameters.Add("method", "vimeo.auth.getFrob");

            AttributesRecord record = this.makeRequest(parameters);

            // If no error occurred
            if (record["status:code"] == "0")
                record.Merge(new AttributesRecord("auth:frob", _responseNodes[0].InnerText));

            return record;
        }

        // Get the token from the server
        private AttributesRecord getToken(string frob)
        {
            if (_apiKey == "" || _apiSecret == "")
                return this.makeStatusRecord("10", "API Key or Secret not available");

            Hashtable parameters = new Hashtable();
            parameters.Add("method", "vimeo.auth.getToken");
            parameters.Add("frob", frob);

            AttributesRecord record = this.makeRequest(parameters);

            // If no error occurred
            if (record["status:code"] == "0")
                record.Merge(new AttributesRecord("auth:token", _responseNodes[0].FirstChild.InnerText));

            return record;
        }

        // Construct the authentication url
        private string getAuthorizationUrl(string frob)
        {
            if (_apiSecret == null)
                return "50";

            string hash = Util.ToMD5(_apiSecret + "api_key" + _apiKey + "frob" + frob + "permsdelete");
            string url = _urlAuth + "?api_key=" + _apiKey + "&perms=delete&frob=" + frob;
            url += "&api_sig=" + hash;

            return url;
        }

        #endregion


        #region objects

        private void findObjectsPaginated(int page, int perPage)
        {
            Hashtable parameters = new Hashtable();

            parameters.Add("method", "vimeo.videos.getUploadedList");
            parameters.Add("page", page.ToString());
            parameters.Add("per_page", perPage.ToString());

            AttributesRecord record = this.makeRequest(parameters);

            if (record["status:code"] != "0")
                throw new Exception("Error finding a page of objects");

            foreach (XmlNode objectNode in _responseNodes[0].ChildNodes)
            {
                AttributesRecord obj = new AttributesRecord(AttributesType.Object);

                obj["object:id"] = objectNode.Attributes["id"].Value;
                obj["annotation:title"] = objectNode.Attributes["title"].Value;

                _objects.Add(obj);
            }
        }

        private void findObjectInfo(AttributesRecord objectRecord)
        {
            Hashtable parameters = new Hashtable();

            parameters.Add("method", "vimeo.videos.getInfo");
            parameters.Add("video_id", objectRecord["object:id"].ToString());

            AttributesRecord record = this.makeRequest(parameters);

            if (record["status:code"] != "0")
                throw new Exception("Error calling getInfo");

            foreach (XmlNode propertyNode in _responseNodes[0].ChildNodes)
            {
                switch (propertyNode.Name)
                {
                    case "title":
                        objectRecord["annotation:title"] = propertyNode.InnerText;
                        break;

                    case "caption":
                        objectRecord["annotation:caption"] = propertyNode.InnerText;
                        break;

                    case "tags":

                        StringBuilder stringBuilder = new StringBuilder();

                        foreach (XmlNode tagNode in propertyNode.ChildNodes)
                            stringBuilder.Append(tagNode.InnerText + ",");

                        stringBuilder.Remove(stringBuilder.Length - 1, 1);

                        objectRecord["annotation:keywords"] = stringBuilder.ToString();

                        break;

                    case "urls":

                        foreach (XmlNode urlNode in propertyNode)
                        {
                            switch (urlNode.Attributes["type"].Value)
                            {
                                case "videopage":
                                    objectRecord["url:html"] = urlNode.InnerText;
                                    break;
                            }
                        }

                        break;
                }

            }
        }

        private void findObjectThumbnailUrl(AttributesRecord objectRecord)
        {
            Hashtable parameters = new Hashtable();

            parameters.Add("method", "vimeo.videos.getThumbnailUrl");
            parameters.Add("video_id", objectRecord["object:id"].ToString());
            parameters.Add("size", "200");

            AttributesRecord record = this.makeRequest(parameters);

            if (record["status:code"] != "0")
                throw new Exception("Error calling getThumbnailUrl");

            objectRecord["url:thumbnail"] = _responseNodes[0].InnerText;
        }

        #endregion


        #region upload

        private void uploadObject(AttributesRecord objectRecord, Stream fileStream)
        {
            string uploadTicket = this.getUploadTicket();

            AttributesRecord status = this.uploadFile(fileStream, uploadTicket);

            // Upload failed
            if (status["status:code"] != "0")
                objectRecord.Merge(status);

            // Upload success
            else
            {
                string objectID = this.getUploadedObjectId(uploadTicket);

                // Could not get id
                if (string.IsNullOrEmpty(objectID))
                    objectRecord.Merge(this.makeStatusRecord("50", "Upload failed"));

                // Found id
                else
                {
                    objectRecord["object:id"] = objectID;

                    // Add title
                    if (objectRecord.Contains("annotation:title"))
                    {
                        try
                        {
                            this.setUploadedObjectTitle(objectID, objectRecord["annotation:title"]);
                        }
                        catch (Exception e)
                        {
                            objectRecord.Merge(this.makeStatusRecord("0", "Could not set title"));
                        }
                    }

                    // Add caption
                    if (objectRecord.Contains("annotation:caption"))
                    {
                        try
                        {
                            this.setUploadedObjectCaption(objectID, objectRecord["annotation:caption"]);
                        }
                        catch (Exception e)
                        {
                            objectRecord.Merge(this.makeStatusRecord("0", "Could not set caption"));
                        }
                    }

                    // Add tags
                    if (objectRecord.Contains("annotation:keywords"))
                    {
                        try
                        {
                            this.setUploadedObjectTags(objectID, objectRecord["annotation:keywords"]);
                        }
                        catch (Exception e)
                        {
                            objectRecord.Merge(this.makeStatusRecord("0", "Could not set keywords"));
                        }
                    }
                }
            }

            if (objectRecord.Contains("object:id"))
                objectRecord["status:code"] = "0";
        }

        private string getUploadTicket()
        {
            Hashtable parameters = new Hashtable();

            parameters.Add("method", "vimeo.videos.getUploadTicket");

            AttributesRecord record = this.makeRequest(parameters);

            if (record["status:code"] != "0")
                throw new Exception("Error getting upload ticket");

            return _responseNodes[0].Attributes["id"].Value;
        }

        private AttributesRecord uploadFile(Stream fileStream, string uploadTicket)
        {
            Hashtable parameters = new Hashtable();

            parameters.Add("api_key", _apiKey);
            parameters.Add("auth_token", _authToken);
            parameters.Add("ticket_id", uploadTicket);

            this.constructParameters(parameters);

            HttpUtil http = new HttpUtil();
            http.ContentType = "multipart/form-data";

            string response = null;
            try
            {
                response = http.SendFile(_urlUpload, parameters, "video", fileStream);
            }
            catch (Exception e)
            {
                return this.makeStatusRecord(http.MSCFStatusCode, e.Message);
            }

            return this.makeStatusRecord("0", null);
        }

        private string getUploadedObjectId(string uploadTicket)
        {
            Hashtable parameters = new Hashtable();

            parameters.Add("method", "vimeo.videos.checkUploadStatus");
            parameters.Add("ticket_id", uploadTicket);

            AttributesRecord record = this.makeRequest(parameters);

            if (record["status:code"] != "0")
                return string.Empty;

            if (_responseNodes[0].Attributes["video_id"] != null)
                return _responseNodes[0].Attributes["video_id"].Value;
            else
                return string.Empty;
        }

        private void setUploadedObjectTitle(string objectID, string title)
        {
            Hashtable parameters = new Hashtable();

            parameters.Add("method", "vimeo.videos.setTitle");
            parameters.Add("video_id", objectID);
            parameters.Add("title", title);

            AttributesRecord record = this.makeRequest(parameters);

            if (record["status:code"] != "0")
                throw new Exception();
        }

        private void setUploadedObjectCaption(string objectID, string caption)
        {
            Hashtable parameters = new Hashtable();

            parameters.Add("method", "vimeo.videos.setCaption");
            parameters.Add("video_id", objectID);
            parameters.Add("caption", caption);

            AttributesRecord record = this.makeRequest(parameters);

            if (record["status:code"] != "0")
                throw new Exception();
        }

        private void setUploadedObjectTags(string objectID, string tags)
        {
            Hashtable parameters = new Hashtable();

            parameters.Add("method", "vimeo.videos.addTags");
            parameters.Add("video_id", objectID);
            parameters.Add("tags", tags);

            AttributesRecord record = this.makeRequest(parameters);

            if (record["status:code"] != "0")
                throw new Exception();
        }

        #endregion


        #region utility_methods

        private void updateUrls(string server)
        {
            _urlRest = server + "api/rest/";
            _urlUpload = server + "services/upload/";
            _urlAuth = server + "services/auth/";
        }

        private AttributesRecord makeRequest(Hashtable parameters)
        {
            // Clear the node list
            _responseNodes = null;

            // Construct the parameters as needed by Flickr API
            this.constructParameters(parameters);

            HttpUtil http = new HttpUtil();

            // Using urlencoded POST requests
            //http.ContentType = "application/x-www-form-urlencoded";

            // Make the HTTP request
            string response = "";
            try
            {
                response = http.SendPost(_urlRest, parameters, true);
            }
            catch (Exception e)
            {
                Console.WriteLine("makeRequest Exception: " + e.Message);
                return this.makeStatusRecord(http.MSCFStatusCode, null);
            }

            switch (http.HttpStatusCode)
            {
                case "200": // OK
                    return this.getRspNodeList(response);
                    break;

                // Authentication error
                case "401":
                case "403":
                    return this.makeStatusRecord("41", null);
                    break;

                default:
                    return this.makeStatusRecord("31", null);
            }
        }

        // Parses the HTTP response to get the list of nodes under the "rsp" node
        private AttributesRecord getRspNodeList(string response)
        {
            AttributesRecord status = this.makeStatusRecord("0", null);

            if (!String.IsNullOrEmpty(response))
            {
                System.Xml.XmlDocument doc = new XmlDocument();
                System.Xml.XmlNode rspNode;
                try
                {
                    doc.LoadXml(response);

                    // Get the 'rsp' node.
                    rspNode = doc.GetElementsByTagName("rsp")[0];

                    // Check for error
                    if (rspNode.Attributes["stat"].Value != "ok")
                        status = this.makeStatusRecord("50", null);
                    else
                        _responseNodes = rspNode.ChildNodes;
                }
                catch (System.Exception wex)
                {
                    // Unexpected response
                    status = this.makeStatusRecord("31", null);
                }
            }

            return status;
        }

        // Add all parameters needed for a request to an API method
        private void constructParameters(Hashtable parameters)
        {
            parameters["api_key"] = _apiKey;

            if (_authToken != null && _authToken.Length > 0)
                parameters["auth_token"] = _authToken;

            // Parameters need to be in alphabetical order for signature
            string[] keys = new string[parameters.Keys.Count];
            parameters.Keys.CopyTo(keys, 0);
            Array.Sort(keys);

            StringBuilder hashStringBuilder = new StringBuilder(_apiSecret, 2 * 1024);

            // Add the parameters to the request and to the signature calulation string
            foreach (string key in keys)
            {
                hashStringBuilder.Append(key);
                hashStringBuilder.Append(parameters[key]);
            }

            // Calculate signature
            if (_apiSecret != null && _apiSecret.Length > 0)
                parameters.Add("api_sig", Util.ToMD5(hashStringBuilder.ToString()));
        }

        // Download either original or thumbnail objects
        private AttributesRecord downloadFromUrl(AttributesRecord photoAttributes, string attributeName, string downloadPath)
        {
            if (!photoAttributes.Contains("file:" + attributeName))
                return this.makeStatusRecord("0", null);

            // Construct the default file name
            string defaultFileName = "";
            if (attributeName == "original")
                defaultFileName = photoAttributes["object:id"] + ".jpg";
            else
                defaultFileName = "th_" + photoAttributes["object:id"] + ".jpg";

            // Download the file
            string filename = photoAttributes["file:" + attributeName];
            if (filename == "")
            {
                if (downloadPath == null || downloadPath == "")
                    return this.makeStatusRecord("10", "No download path specified"); //invalid params
                downloadPath += defaultFileName;
            }
            else if (filename[filename.Length - 1] == '\\')
                // File attribute is a folder path. Download to this folder.
                downloadPath = filename + defaultFileName;
            else
                // File attribute is an absolute path. Download to this path.
                downloadPath = filename;

            AttributesRecord status = this.downloadFile(photoAttributes["url:" + attributeName], downloadPath);

            if (status["status:code"] == "0")
                photoAttributes["file:" + attributeName] = downloadPath;

            return status;
        }

        // Download a file from a url
        private AttributesRecord downloadFile(string url, string fileName)
        {
            try
            {
                FileDownloader downloader = new FileDownloader();

                if (File.Exists(fileName))
                    File.Delete(fileName);

                downloader.Download(url, fileName);
                //File.Move(downloader.DownloadingTo, fileName);

                return this.makeStatusRecord("0", null);
            }
            catch (Exception e)
            {
                return this.makeStatusRecord("50", e.Message);
            }
        }

        private AttributesRecord makeStatusRecord(string code, string message)
        {
            AttributesRecord status = new AttributesRecord(AttributesType.Sock);
            status["status:code"] = code;
            if (message != null)
                status["status:message"] = message;
            return status;
        }

        #endregion
    }
}
